package transporte.server.dao.impl;

import java.lang.reflect.ParameterizedType;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.LockMode;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

/**
 * This class serves as the Base class for all other Daos - namely to hold common methods that they might all use. Can be used for standard CRUD operations.
 * </p>
 * 
 * <p>
 * <a href="BaseDaoHibernate.java.html"><i>View Source</i></a>
 * </p>
 * 
 * 
 */
public class BaseDaoHibernate<T> extends HibernateDaoSupport {
	protected final Log log = LogFactory.getLog(getClass());
	private Class<T> persistentClass;
	
	/**
	 * 
	 */
	public BaseDaoHibernate() {
		super();
	}
	
	/**
	 * @param sessionFactory
	 */
	public BaseDaoHibernate(SessionFactory sessionFactory) {
		super.setSessionFactory(sessionFactory);
		 this.persistentClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
	}
	
	
	/**
	 * @return the persistentClass
	 */
	public Class<T> getPersistentClass() {
		return persistentClass;
	}


	/**
	 * Saves any hibernate pojo into the database
	 * 
	 * @param o
	 * @author juanm
	 */
	protected void saveObject(T o) {
		getHibernateTemplate().saveOrUpdate(o);
	}
	
	/**
	 * Gets any hibernate pojo from the db
	 * @param clazz
	 * @param id
	 * @return
	 * @author juanm
	 */
	protected T getObject(Class clazz, long id, LockMode lockMode) {
		T o = (T)getHibernateTemplate().get(clazz, Long.valueOf(id), lockMode);

		if (o == null) {
			throw new ObjectRetrievalFailureException(clazz, Long.valueOf(id));
		}
		return o;
	}

	
	/**
	 * Removes an object from the session cache
	 * @param o
	 * 
	 */
	protected void evict(T o) {
		if (getHibernateTemplate().contains(o))
			getHibernateTemplate().evict(o);
	}
	
	/**
	 * Attach an object from the session cache
	 * @param o
	 * 
	 */
	protected void attach(T o) {
		getHibernateTemplate().lock(o, LockMode.NONE);
	}

	/**
	 * Gets an object by his ID
	 * 
	 * @author spulido
	 * @param inventoryId
	 * @return
	 */
	public T getById(long id) {
		return this.load(id);
	}
	
	/**
	 * Loads an object. The difference is that the object is only readed when readed. That means
	 * less selects are executed to the db. Entities are not fechted when the a relation is created
	 * @param id
	 * @return
	 * 
	 */
	protected T load(long id) {
		return (T)getHibernateTemplate().load(getPersistentClass(), Long.valueOf(id));
	}
	
	/**
	 * Gets any hibernate pojo from the db
	 * 
	 * @param id
	 * @return
	 * @author juanm
	 */
	protected T getObject(long id) {
		return load(id);
	}
	
	/**
	 * Gets any hibernate pojo from the db
	 * 
	 * @param clazz
	 * @param id
	 * @return
	 * @author juanm
	 */
	protected T getObject(Class clazz, long id) {
		return (T)getHibernateTemplate().load(clazz, Long.valueOf(id));
	}
	

	/**
	 * Get a list of hibernate pojos from the db
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjects() {
		return getHibernateTemplate().loadAll(getPersistentClass());
	}
	
	
	/**
	 * Deletes an object from the db
	 * @param id
	 * @author juanm
	 */
	protected void deleteObject(long id) {
		deleteObject(getObject(id));
	}
	
	/**
	 * Deletes an object
	 * @param t
	 * 
	 */
	protected void deleteObject(T t) {
		getHibernateTemplate().delete(t);
	}

	/**
	 * Gets any object by a give param
	 * @param propertyName
	 * @param propertyValue
	 * @return
	 * @author juanm
	 */
	protected T getObjectByParam(String propertyName, Object propertyValue) {
		DetachedCriteria criteria = DetachedCriteria.forClass(getPersistentClass()).add(Property.forName(propertyName).eq(propertyValue));
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
		if (results == null || results.isEmpty())
			return null;
		if (results.size() > 1)
			throw new ObjectRetrievalFailureException(getPersistentClass(), propertyValue);

		return results.get(0);
	}
	
	/**
	 * Gets any object by a list of params
	 * @param propertyName
	 * @param propertyValue
	 * @return
	 * @author juanm
	 */
	protected T getObjectByParams(Map<String, Object> params) {
		if (params == null)
			throw new NullPointerException("params can't be null");
		
		DetachedCriteria criteria = DetachedCriteria.forClass(getPersistentClass());
		StringBuilder paramValues = new StringBuilder();
		for (Map.Entry<String, Object> entry : params.entrySet()) {
			criteria.add(Property.forName(entry.getKey()).eq(entry.getValue()));
			
			if (paramValues.length()>0)
				paramValues.append(",");
			
			paramValues.append(entry.getKey())
				.append(":")
				.append(entry.getValue());
		}
		
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
		
		if (results.size() > 1)
			throw new ObjectRetrievalFailureException(getPersistentClass(), paramValues.toString());
		
		if (results!=null && !results.isEmpty())
			return results.get(0);
		else
			return null;
	}
	
	
	/**
	 * Finds a list of object using a sample instance of the object
	 * @param exampleInstance
	 * @param excludeProperty
	 * @return
	 * 
	 */
    protected List<T> getByExample(T exampleInstance, String... excludeProperty) {
        Criteria crit = getSession().createCriteria(getPersistentClass());
        Example example =  Example.create(exampleInstance);
        for (String exclude : excludeProperty) {
            example.excludeProperty(exclude);
        }
        crit.add(example);
        return crit.list();
    }
    
    /**
	 * Gets a list of objects by a given param
	 * @param paramName
	 * @param paramValue
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParam(String paramName, Object paramValue) {
		return getObjectsByParam(paramName, paramValue, null);
	}
	
	/**
	 * Gets a list of objects by a given param
	 * @param paramName
	 * @param paramValue
	 * @param orderBy
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParam(String paramName, Object paramValue, String orderBy) {
		DetachedCriteria criteria = DetachedCriteria.forClass(getPersistentClass()).add(Property.forName(paramName).eq(paramValue));
		if (StringUtils.isNotBlank(orderBy))
			criteria.addOrder(Order.asc(orderBy));
		
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
		
		return results;
	}
	
	/**
	 * Gets a list of objects by a map of params
	 * @param params propertyName:propertyValue 
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParams(Map<String, Object> params) {
		return getObjectsByParams(params, null, null);
	}
	
	/**
	 * Gets a list of objects by a map of params
	 * @param params propertyName:propertyValue
	 * @param orderBy 
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParams(Map<String, Object> params, String orderBy) {
		return getObjectsByParams(params, null, orderBy);
	}
	
	/**
	 * Gets a list of objects by a map of params
	 * @param params propertyName:propertyValue
	 * @param nullParams 
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParams(Map<String, Object> params, Map<String, Object> nullParams) {
		return getObjectsByParams(params, nullParams, null);
	}
	
	/**
	 * Gets a list of objects by a map of params
	 * @param params propertyName:propertyValue
	 * @param nullParams 
	 * @param orderBy
	 * @return
	 * @author juanm
	 */
	protected List<T> getObjectsByParams(Map<String, Object> params, Map<String, Object> nullParams, String orderBy) {
		if (params == null)
			throw new NullPointerException("params can't be null");
		
		DetachedCriteria criteria = DetachedCriteria.forClass(getPersistentClass());
		for (Map.Entry<String, Object> entry : params.entrySet()) {
			criteria.add(Property.forName(entry.getKey()).eq(entry.getValue()));
		}
		
		if (nullParams != null) {
			for (Map.Entry<String, Object> entry : nullParams.entrySet()) {
				criteria.add(Property.forName(entry.getKey()).isNull());
			}
		}
		
		if (StringUtils.isNotBlank(orderBy))
			criteria.addOrder(Order.asc(orderBy));
		
		List<T> results = getHibernateTemplate().findByCriteria(criteria);
		
		return results;
	}
	
	/**
	 * Execute the named query queryname with the given params
	 * 
	 * @param queryname
	 * @param params
	 * @return
	 */
	protected List<T> getObjectsByNamedQuery(String queryname, Object[] params) {
		return getHibernateTemplate().findByNamedQuery(queryname, params);
	}
	
	/**
	 * Get one and only one object using the named query
	 * @param queryname
	 * @param params
	 * @return
	 * 
	 */
	protected T getObjectByNamedQuery(String queryname, Object[] params) {
		List<T> results = getObjectsByNamedQuery(queryname, params);
		if (results == null)
			throw new IncorrectResultSizeDataAccessException(1);
		if (results.size() > 1 || results.isEmpty())
			throw new IncorrectResultSizeDataAccessException(1, results.size());

		return results.get(0);
	}
	
	/**
	 * Finds the size of a collection
	 * @param collection
	 * @return
	 * 
	 */
	public int size(Collection<?> collection) {
		return ((Long) super.getSession().createFilter( collection, "select count(*)" ).uniqueResult()).intValue();
	}

	public int size(Class clazz, Map<String, Object> params) {
		
		Criteria c = this.getSession()
					.createCriteria(clazz).setProjection(Projections.rowCount());
		
		Set<String> keys = params.keySet();
		for (String key : keys) {
			Object param = params.get(key);
			c.add(Property.forName(key).eq(param));
		}
		return ((BigInteger) c.uniqueResult()).intValue();
	}

	/**
	 * Empty save method
	 * 
	 * @author spulido
	 * @param t
	 */
	public void save(T t) {
		throw new RuntimeException("Save method not implemented for "+this.getClass());
    }
}